﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Globalization;
using System.Security.Cryptography;
using System.IO;
using System.Diagnostics;

using Legend.IO;
using System.Web;

namespace Legend.Web.QueryString
{
    /// <summary>
    /// Handles the serialization to and from an encrypted query string.
    /// </summary>
    public class EncryptedQueryStringSerializer
        : IQueryStringSerializer
    {
        private string encryptionKeyField;
        private IQueryStringSerializer outputSerializer;
        private string queryStringKeyField;
        private byte[] keyBytes;
        private byte[] initializationVector = { 38, 36, 19, 24, 87, 96, 62, 65, 124, 109, 84, 81, 78, 95, 5, 16 };
        private byte[] passwordSalt = { 85, 70, 109, 37, 64, 126, 56, 116 };

        /// <summary>
        /// Creates a new instance.
        /// </summary>
        /// <param name="encryptionKey">The key to use for encryption.</param>
        /// <param name="queryStringKey">The key used in the query string for the encrypted value.</param>
        public EncryptedQueryStringSerializer(string encryptionKey, string queryStringKey)
        {
            this.EncryptionKey = encryptionKey;
            this.QueryStringKey = queryStringKey;

            this.outputSerializer = new QueryStringSerializer();
        }

        /// <summary>
        /// Creates a new instance.
        /// </summary>
        /// <param name="encryptionKey">The key to use for encryption.</param>
        public EncryptedQueryStringSerializer(string encryptionKey)
            : this(encryptionKey, "data")
        { }

        /// <summary>
        /// The key used for the encryption.
        /// </summary>
        public string EncryptionKey
        {
            get
            {
                return this.encryptionKeyField;
            }
            set
            {
                Require.ThatArgument(value).Named("value").IsNotNullOrEmpty();
                this.encryptionKeyField = value;
                SetKeyBytes(value);
            }
        }

        /// <summary>
        /// The key used in the query string for the encrypted value.
        /// </summary>
        public string QueryStringKey
        {
            get
            {
                return this.queryStringKeyField;
            }
            set
            {
                Require.ThatArgument(value).Named("value").IsNotNullOrEmpty();
                this.queryStringKeyField = value;
            }
        }

        /// <summary>
        /// Parse a query string that has been serialized using the Serialize method.
        /// </summary>
        /// <param name="queryString">The query string to parse.</param>
        /// <returns>A collection of key value pairs found in the decrypted query string.</returns>
        public IDictionary<string, string> Parse(string queryString)
        {
            Require.ThatArgument(queryString).Named("queryString").IsNotNull();

            var start = string.Concat(this.QueryStringKey, "=");
            if (!queryString.StartsWith(start, StringComparison.Ordinal))
            {
                throw new FormatException("The specified string is not a correctly formatted encrypted query string.");
            }

            var encryptedValue = queryString.Substring(start.Length, queryString.Length - start.Length);
            var encryptedBytes = GetBytesFromUrlEncodedString(encryptedValue);
            byte[] decryptedBytes = this.DecryptByteValueFromQueryString(encryptedBytes);
            byte[] decompressedBytes = Decompress(decryptedBytes);

            return this.outputSerializer.Parse(Encoding.Unicode.GetString(decompressedBytes));
        }

        /// <summary>
        /// Serializes the specified key value pairs to an encrypted query string.
        /// </summary>
        /// <param name="keyValuePairs">The key value pairs representing the query string.</param>
        /// <returns>An encrypted query string.</returns>
        public string Serialize(IDictionary<string, string> keyValuePairs)
        {
            Require.ThatArgument(keyValuePairs).Named("keyValuePairs").IsNotNull();

            var queryString = this.outputSerializer.Serialize(keyValuePairs);

            return string.Format(CultureInfo.InvariantCulture, "{0}={1}",
                this.QueryStringKey,
                this.EncryptString(queryString));
        }

        private void SetKeyBytes(string value)
        {
            var passwordGenerator = new PasswordDeriveBytes(
                value, this.passwordSalt);
            this.keyBytes = passwordGenerator.GetBytes(16);
        }

        private byte[] DecryptByteValueFromQueryString(byte[] queryStringValue)
        {
            using (var transform = this.CreateCryptoTransform())
            using (var decryptor = transform.CreateDecryptor())
            {
                return  decryptor.TransformFinalBlock(queryStringValue, 0, queryStringValue.Length);
            }
        }

        private static byte[] Decompress(byte[] compressedBytes)
        {
            using (var input = new MemoryStream(compressedBytes))
            using (var compressionStream = new System.IO.Compression.DeflateStream(input, System.IO.Compression.CompressionMode.Decompress))
            {
                return compressionStream.ReadToEnd(compressedBytes.Length);
            }
        }

        private string EncryptString(string stringToEncrypt)
        {
            var stringBytes = Encoding.Unicode.GetBytes(stringToEncrypt);

            var compressedBytes = CompressBytes(stringBytes);

            byte[] encryptedBytes = null;
            using (var encryptor = CreateCryptoTransform().CreateEncryptor())
            {
                encryptedBytes = encryptor.TransformFinalBlock(
                    compressedBytes, 0, compressedBytes.Length);
            }

            
            return ToUrlEncodedString(encryptedBytes);
        }

        private static string ToUrlEncodedString(byte[] bytes)
        {
            return HttpUtility.UrlEncode(Convert.ToBase64String(bytes));
        }

        private static byte[] GetBytesFromUrlEncodedString(string urlEncodedString)
        {
            return Convert.FromBase64String(HttpUtility.UrlDecode(urlEncodedString));
        }

        private static byte[] CompressBytes(byte[] buffer)
        {
            byte[] result = null;

            using (var output = new MemoryStream())
            using (var compressionStream = new System.IO.Compression.DeflateStream(output, System.IO.Compression.CompressionMode.Compress))
            {
                compressionStream.Write(buffer, 0, buffer.Length);
                result = output.GetBuffer();    
            }

            return result;
        }

        private SymmetricAlgorithm CreateCryptoTransform()
        {
            var rijndahl = new RijndaelManaged();
            rijndahl.Key = this.keyBytes;
            rijndahl.IV = this.initializationVector;

            return rijndahl;
        }
    }
}
